﻿using OpenTK;
using OpenTK.Graphics.OpenGL;
using System;
using System.Collections.Generic;

namespace LDLib
{
    /// <summary>
    /// Represents a 3D mesh that consists of vertices.
    /// </summary>
    public class Mesh
    {
        /// <summary>
        /// List of vertices
        /// </summary>
        private List<Vertex> vertices;

        /// <summary>
        /// Retrieves the list of vertices
        /// </summary>
        public List<Vertex> Vertices { get { return vertices; } }

        /// <summary>
        /// List of indices
        /// </summary>
        private List<int> indices;

        /// <summary>
        /// Retrieves the list of indices
        /// </summary>
        public List<int> Indices { get { return indices; } }

        /// <summary>
        /// Vertex buffer ID
        /// </summary>
        private int id_vb = -1;

        /// <summary>
        /// Retrieves the vertex buffer object ID
        /// </summary>
        public int VBO { get { return id_vb; } }

        /// <summary>
        /// Index buffer ID
        /// </summary>
        private int id_ib = -1;

        // Bounding sphere
        private Vector3 center = Vector3.Zero;
        public Vector3 Center { get { return center; } }
        private float radius = 0f;
        public float Radius { get { return radius; } }

        /// <summary>
        /// Gets or sets the 'should reload' status of this mesh.
        /// If this is 'true', the mesh will be re-uploaded to the
        /// GPU memory before rendering it the next time.
        /// </summary>
        public bool ShouldReload { get; set; }

        /// <summary>
        /// Constructs a new mesh
        /// </summary>
        public Mesh()
        {
            this.vertices = new List<Vertex>();
            this.indices = new List<int>();
        }

        /// <summary>
        /// Constructs a new mesh
        /// </summary>
        /// <param name="vertices">List of vertices</param>
        /// <param name="indices">List of indices</param>
        public Mesh(List<Vertex> vertices, List<int> indices)
        {
            this.vertices = vertices;
            this.indices = indices;
        }

        /// <summary>
        /// Calculates the tangent and bitangent based on the
        /// normal and UV data
        /// </summary>
        public void CalculateTBN()
        {
            for (int i = 0; i < indices.Count; i += 3)
            {
                Vertex v0 = vertices[(int)indices[i]];
                Vertex v1 = vertices[(int)indices[i + 1]];
                Vertex v2 = vertices[(int)indices[i + 2]];

                Vector3 dPos1 = v1.position - v0.position;
                Vector3 dPos2 = v2.position - v0.position;

                Vector2 dUV1 = v1.uv - v0.uv;
                Vector2 dUV2 = v2.uv - v0.uv;

                float r = 1.0f / (dUV1.X * dUV2.Y - dUV1.Y * dUV2.X);
                Vector3 tangent = (dPos1 * dUV2.Y - dPos2 * dUV1.Y) * r;
                Vector3 bitangent = (dPos2 * dUV1.X - dPos1 * dUV2.X) * r;

                tangent.Normalize();
                bitangent.Normalize();

                if (Vector3.Dot(Vector3.Cross(v0.normal, tangent), bitangent) < 0f)
                {
                    tangent *= -1f;
                }

                v0.tangent = tangent;
                v1.tangent = tangent;
                v2.tangent = tangent;
                v0.bitangent = bitangent;
                v1.bitangent = bitangent;
                v2.bitangent = bitangent;

                vertices[(int)indices[i]] = v0;
                vertices[(int)indices[i + 1]] = v1;
                vertices[(int)indices[i + 2]] = v2;
            }
        }

        /// <summary>
        /// Setups the mesh. The process involves calculating the
        /// bounding sphere of the mesh, creating the vertex and
        /// element buffers and binding the data to those buffers.
        /// </summary>
        public void SetupMesh()
        {
            // Calculate the bounding sphere
            foreach (Vertex v in vertices)
            {
                center += v.position;
            }
            foreach (Vertex v in vertices)
            {
                radius = Math.Max(radius, Vector3.Distance(v.position, center));
            }

            // Vertices
            id_vb = GL.GenBuffer();
            GL.BindBuffer(BufferTarget.ArrayBuffer, id_vb);

            GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Vertex.Size() * vertices.Count), vertices.ToArray(), BufferUsageHint.StaticDraw);
            SetVertexAttribArrays();

            // Indices
            id_ib = GL.GenBuffer();
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, id_ib);
            GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(sizeof(int) * indices.Count), indices.ToArray(), BufferUsageHint.StaticDraw);

        }

        /// <summary>
        /// Enabled the vertex attribute arrays and sets the attribute pointers
        /// </summary>
        private void SetVertexAttribArrays()
        {
            GL.EnableVertexAttribArray(4);
            GL.EnableVertexAttribArray(3);
            GL.EnableVertexAttribArray(2);
            GL.EnableVertexAttribArray(1);
            GL.EnableVertexAttribArray(0);

            GL.VertexAttribPointer(4, 3, VertexAttribPointerType.Float, false, Vertex.Size(), Vector3.SizeInBytes * 3 + Vector2.SizeInBytes);
            GL.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, Vertex.Size(), Vector3.SizeInBytes * 2 + Vector2.SizeInBytes);
            GL.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, Vertex.Size(), Vector3.SizeInBytes * 2);
            GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, Vertex.Size(), Vector3.SizeInBytes);
            GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, Vertex.Size(), 0);
        }

        /// <summary>
        /// Renders the mesh.
        /// </summary>
        public void RenderMesh()
        {
            GL.BindBuffer(BufferTarget.ArrayBuffer, id_vb);
            GL.BindBuffer(BufferTarget.ElementArrayBuffer, id_ib);
            if (ShouldReload)
            {
                GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Vertex.Size() * vertices.Count), vertices.ToArray(), BufferUsageHint.StreamDraw);
                GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(sizeof(uint) * indices.Count), indices.ToArray(), BufferUsageHint.DynamicDraw);
                ShouldReload = false;
            }
            SetVertexAttribArrays();
            GL.DrawElements(BeginMode.Triangles, indices.Count, DrawElementsType.UnsignedInt, 0);
        }
    }
}
